﻿/**************************************************************
 * 
 * Copyright © Dmitry Kukushkin <codecity@gmail.com> 2007
 * All Rights Reserved
 * 
 **************************************************************/

using System;
using WebMoney.Cryptography;
using WebMoney.SignerFX.Exceptions;

namespace WebMoney.SignerFX
{
    /// <summary>
    /// Представление WebMoney ключей
    /// </summary>
    public sealed class Keys : BKeys
    {
        private const int UINT_BYTES = 4;

        // ***** Файл ключей (сохранена оригинальная структура) *****

        //private const int RESERVED_START = 0;
        //private const int RESERVED_LEN = 2;
        private const int SIGN_FLAG_START = 2;
        private const int SIGN_FLAG_LEN = 2;
        private const int RECIEVED_START = 4;
        private const int RECIEVED_LEN = 16;
        private const int LEN_CHECKED_START = 20;
        //private const int LEN_CHECKED_LEN = 4;
        //private const int FULL_KEYS_START = 24;
        //private const int FULL_KEYS_INDENT = 6;
        private const int EXP_KEY_LEN_START = 28;
        //private const int EXP_KEY_LEN_LEN = 2;
        private const int EXP_KEY_START = 30;

        const int HEADER_LEN = 24;
        const int HASH_LEN = 16;

        const int KEYS_DATA_BYTES = 164; // длина файла ключей
        const int ID_LEN = 12; // длина WMID
        const int MIN_PW_LEN = 1; // минимальная длина пароля

        readonly byte[] _keysData; // ключи
        readonly string _id; // WMID
        readonly string _password; // пароль
        readonly MD4 _md4;

        /// <summary>
        /// Инициализация объекта
        /// </summary>
        /// <param name="keysData">Файл ключей в виде байтового массива</param>
        /// <param name="id">WMID</param>
        /// <param name="password">Пароль</param>
        internal Keys(byte[] keysData, string id, string password)
        {
            if (null == keysData)
                throw new ArgumentNullException("key");
            
            if (null == id)
                throw new ArgumentNullException("id");
            
            if (null == password)
                throw new ArgumentNullException("password");

            if (KEYS_DATA_BYTES != keysData.Length) // поддерживаются только ключи размером 164 байта
                throw new KeysSizeException("KEYS_DATA_BYTES != keysData.Length");

            if(ID_LEN != id.Length)
                throw new ArgumentOutOfRangeException("id");

            if(MIN_PW_LEN > password.Length)
                throw new ArgumentOutOfRangeException("password");

            _keysData = new byte[KEYS_DATA_BYTES]; // не меняем исходные данные, создаем копию
            keysData.CopyTo(_keysData, 0);

            _id = id;
            _password = password;
            _md4 = new MD4();

            // Очистка Sign Flag
            Array.Clear(_keysData, SIGN_FLAG_START, SIGN_FLAG_LEN);
            // Создаем резервную копию ключей
            byte[] reservKeys = new byte[KEYS_DATA_BYTES - EXP_KEY_START];
            Array.Copy(_keysData, EXP_KEY_START, reservKeys, 0, KEYS_DATA_BYTES - EXP_KEY_START);

            restoreKeys(true); // попытка восстановить ключи XOR(MD4(логин + половина пароля))

            bool isGoodData = checkKeysData(); // проверка
            // Если ключи не восстановились
            if(!isGoodData)
            {
                // Возвращаем ключи к прежнему состоянию из резервной копии
                Array.Copy(reservKeys, 0, _keysData, EXP_KEY_START, KEYS_DATA_BYTES - EXP_KEY_START);
                // Попытка восстановить ключи XOR(MD4(логин + пароль))
                restoreKeys(false);

                isGoodData = checkKeysData(); // проверка

                if (!isGoodData) // если не восстановились — файл ключей имеет неверный формат
                    throw new BadKeysException("recievedHash != checkedHash", "Keys.Keys");
                else
                    loadKeys();
            }
            else
                loadKeys();
        }

        // Восстанавливаем ключи
        void restoreKeys(bool halfPassw)
        {
            string logPassw = _id;
            logPassw += (halfPassw) ? _password.Substring(0, _password.Length / 2) : _password;

            _md4.Initialize();
            byte[] logPasswHash = _md4.GetHash(logPassw);

            for (int pos = EXP_KEY_START; pos < KEYS_DATA_BYTES; pos++)
                _keysData[pos] ^= logPasswHash[(pos - EXP_KEY_START) % HASH_LEN];
        }

        // Проверка файла ключей
        bool checkKeysData()
        {
            // Сохраняем RECIEVED для последующего восстановления
            uint[] recievedHash = new uint[RECIEVED_LEN / UINT_BYTES];
            Buffer.BlockCopy(_keysData, RECIEVED_START, recievedHash, 0, RECIEVED_LEN);
            // очищаем RECIEVED
            Array.Clear(_keysData, RECIEVED_START, RECIEVED_LEN);
            // Узнаем длину проверяемых данных (длина uint соответствует длине LEN_CHECKED (4 байта))
            uint checkedLen = BitConverter.ToUInt32(_keysData, LEN_CHECKED_START);
            // Она не может быть меньше KEYS_DATA_BYTES - HEADER_LEN
            if (KEYS_DATA_BYTES - HEADER_LEN < checkedLen)
                throw new BadKeysException("KEYS_DATA_BYTES - HEADER_LEN < checkedLen");
            // Добавляем заголовок
            checkedLen += HEADER_LEN;
            // Вычисляем хеш-значение проверяемых данных
            _md4.Initialize();
            uint[] checkedHash = _md4.GetUint32Hash(_keysData, 0, (int)checkedLen);
            // Сохраняем RECIEVED назад (на тот случай, если метод checkKeysData будет вызван повторно)
            Buffer.BlockCopy(recievedHash, 0, _keysData, RECIEVED_START, RECIEVED_LEN);
            
            for(int pos = 0; pos < HASH_LEN / UINT_BYTES; pos++)
                if (recievedHash[pos] != checkedHash[pos])
                    return false;

            return true;
        }

        // Загружаем ключи
        void loadKeys()
        {
            int pos = EXP_KEY_LEN_START;
            ushort expKeyLen = BitConverter.ToUInt16(_keysData, EXP_KEY_LEN_START);

            pos += 2; // увеличиваем на длину expKeyLen в байтах
            // Степень
            ExpKey = new uint[(expKeyLen + UINT_BYTES - 1) / UINT_BYTES];
            Buffer.BlockCopy(_keysData, EXP_KEY_START, ExpKey, 0, expKeyLen);

            pos += expKeyLen; // увеличиваем на длину ExpKey в байтах

            ushort modKeyLen = BitConverter.ToUInt16(_keysData, pos);

            pos += 2; // увеличиваем на длину modKeyLen в байтах
        
            if (expKeyLen != modKeyLen)
                throw new BadKeysException("expKeyLen != modKeyLen");
            // Модуль
            ModKey = new uint[(modKeyLen + UINT_BYTES - 1) / UINT_BYTES];
            Buffer.BlockCopy(_keysData, pos, ModKey, 0, modKeyLen);
        }
    }
}